package tech.mhuang.pacebox.core.util;

import tech.mhuang.pacebox.core.compress.DynamicFile;
import tech.mhuang.pacebox.core.compress.DynamicRenameFile;
import tech.mhuang.pacebox.core.compress.RenameFile;
import tech.mhuang.pacebox.core.io.IOUtil;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * zip压缩工具
 *
 * @author yuanhang.huang
 * @since 1.0.7
 */
public class ZipUtil {

    /**
     * 压缩成ZIP 方法
     *
     * @param srcDir           压缩文件夹路径
     * @param out              压缩文件输出流
     * @param keepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws RuntimeException 压缩失败会抛出运行时异常
     * @throws IOException      IO异常
     */
    public static void toZip(String srcDir, OutputStream out, boolean keepDirStructure) throws RuntimeException, IOException {
        try (ZipOutputStream zos = new ZipOutputStream(out);) {
            File sourceFile = new File(srcDir);
            compress(sourceFile, zos, sourceFile.getName(), keepDirStructure);
        }
    }

    /**
     * 压缩成ZIP 方法
     *
     * @param srcFiles 需要压缩的文件列表
     * @param out      压缩文件输出流
     * @throws IOException 压缩失败会抛出运行时异常
     */
    public static void toZip(List<File> srcFiles, OutputStream out) throws IOException {
        try (ZipOutputStream zos = new ZipOutputStream(out)) {
            for (File srcFile : srcFiles) {
                zos.putNextEntry(new ZipEntry(srcFile.getName()));
                try (FileInputStream in = new FileInputStream(srcFile)) {
                    IOUtil.copyLarge(in, zos);
                    zos.closeEntry();
                }
            }
        }
    }


    /**
     * 递归压缩方法
     *
     * @param sourceFile       源文件
     * @param zos              zip输出流
     * @param name             压缩后的名称
     * @param keepDirStructure 是否保留原来的目录结构,true:保留目录结构;
     *                         false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws Exception
     */
    private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean keepDirStructure) throws IOException {
        if (sourceFile.isFile()) {
            // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
            zos.putNextEntry(new ZipEntry(name));
            // copy文件到zip输出流中
            try (FileInputStream in = new FileInputStream(sourceFile)) {
                IOUtil.copyLarge(in, zos);
                // Complete the entry
                zos.closeEntry();
            }
        } else {
            //是文件夹
            File[] listFiles = sourceFile.listFiles();
            if (CollectionUtil.isEmpty(listFiles)) {
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if (keepDirStructure) {
                    // 空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name + File.separator));
                    // 没有文件，不需要文件的copy
                    zos.closeEntry();
                }
            } else {
                for (File file : listFiles) {
                    // 判断是否需要保留原来的文件结构
                    String fileName;
                    if (keepDirStructure) {
                        fileName = new StringBuilder().append(name).append(File.separator).append(file.getName()).toString();
                        // 注意：file.getName()前面需要带上父文件夹的名字加一斜杠,
                        // 不然最后压缩包中就不能保留原来的文件结构,即：所有文件都跑到压缩包根目录下了
                    } else {
                        fileName = file.getName();
                    }
                    compress(file, zos, fileName, keepDirStructure);
                }
            }
        }
    }

    /**
     * zip解压
     *
     * @param sourceFilePath zip源文件
     * @param destDirPath    解压后的目标文件夹
     * @throws IOException IOException
     */
    public static void unZip(String sourceFilePath, String destDirPath) throws IOException {
        unZip(new File(sourceFilePath), destDirPath);
    }

    /**
     * zip解压
     *
     * @param sourceFile  zip源文件
     * @param destDirPath 解压后的目标文件夹
     * @throws IOException IOException
     */
    public static void unZip(File sourceFile, String destDirPath) throws IOException {
        unZip(sourceFile, destDirPath, false);
    }

    /**
     * zip解压
     *
     * @param sourceFilePath    zip源文件路径
     * @param destDirPath   解压后的目标文件夹
     * @param deep  是否深度解压
     * @throws IOException  IOException
     */
    public static void unZip(String sourceFilePath, String destDirPath, boolean deep) throws IOException {
        unZip(new File(sourceFilePath), destDirPath, deep);
    }

    /**
     * zip解压
     *
     * @param sourceFile    zip源文件
     * @param destDirPath   解压后的目标文件夹
     * @param deep  是否深度解压
     * @throws IOException IO异常
     */
    public static void unZip(File sourceFile, String destDirPath, boolean deep) throws IOException {
        // 判断源文件是否存在
        if (!sourceFile.exists()) {
            throw new FileNotFoundException(sourceFile.getPath() + " is not found");
        }
        // 开始解压
        try (ZipFile zipFile = new ZipFile(sourceFile)) {
            List<File> zips = new ArrayList<>();
            Enumeration<?> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = (ZipEntry) entries.nextElement();
                // 如果是文件夹，就创建个文件夹
                if (entry.isDirectory()) {
                    String dirPath = destDirPath + "/" + entry.getName();
                    File dir = new File(dirPath);
                    dir.mkdirs();
                } else {
                    // 如果是文件，就先创建一个文件，然后用io流把内容copy过去
                    File targetFile = new File(destDirPath + "/" + entry.getName());
                    // 如果是zip文件，则放入zips文件列表，用于后续做递归解压
                    if (entry.getName().toLowerCase().endsWith(".zip") && deep) {
                        zips.add(targetFile);
                    }
                    // 保证这个文件的父文件夹必须要存在
                    if (!targetFile.getParentFile().exists()) {
                        targetFile.getParentFile().mkdirs();
                    }
                    targetFile.createNewFile();
                    // 将压缩文件内容写入到这个文件中
                    try (InputStream is = zipFile.getInputStream(entry); FileOutputStream fos = new FileOutputStream(targetFile)) {
                        IOUtil.copyLarge(is, fos);
                    }
                }
            }
            // 递归解压
            if(zips.size() > 0 && deep) {
                for (File zip : zips) {
                    Integer index = zip.getPath().lastIndexOf(".");
                    String path = zip.getPath().substring(0, index);
                    unZip(zip, path, deep);
                }
            }
        }
    }


    /**
     * 动态文件列表转output
     *
     * @param dynamicFileList 动态文件列表
     * @param os              output
     * @since 1.1.6
     */
    public static void toDynamicZip(List<DynamicFile> dynamicFileList, OutputStream os) {
        try (ZipOutputStream zos = new ZipOutputStream(os)) {
            for (DynamicFile fileDTO : dynamicFileList) {
                for (File srcFile : fileDTO.getFileList()) {
                    zos.putNextEntry(new ZipEntry(File.separator + fileDTO.getDirName() + File.separator + srcFile.getName()));
                    try (FileInputStream in = new FileInputStream(srcFile)) {
                        IOUtil.copyLarge(in, zos);
                        zos.closeEntry();
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 动态重命名文件列表转转output
     *
     * @param dynamicRenameFileList 动态重命名文件列表
     * @param os                    output
     * @since 1.1.6
     */
    public static void toDynamicRenameZip(List<DynamicRenameFile> dynamicRenameFileList, OutputStream os) {
        try (ZipOutputStream zos = new ZipOutputStream(os)) {
            for (DynamicRenameFile fileDTO : dynamicRenameFileList) {
                for (RenameFile renameFile : fileDTO.getFileList()) {
                    zos.putNextEntry(new ZipEntry(fileDTO.getDirName() + File.separator + renameFile.getRename()));
                    try (FileInputStream in = new FileInputStream(renameFile.getFile())) {
                        IOUtil.copyLarge(in, zos);
                        zos.closeEntry();
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
